/////////////////////////////////////////////////////////////////////////////
// Name:        ax_module_book.cpp
// Purpose:     Book module classes
// Author:      Julian Smart
// Modified by:
// Created:     2009-03-20
// RCS-ID:
// Copyright:   (c) Julian Smart
// Licence:     New BSD License
/////////////////////////////////////////////////////////////////////////////


/*

  TODO: we need to show play controls on Summary window. We should probably
  have Read All shortcut for all text controls.
  
  Problem: if we tab to the relevant control, we lose our place
  while speaking the controls. Maybe we need another speech
  object - pause first, speak controls, then resume.
  Can't do this with process-based speech objects unless we can
  suspend the process.

  If we tab into text control while reading it, we should probably not
  start describing the text control. So may need a special adapter
  for the book.

 */

#include "wx/wx.h"
#include "wx/wfstream.h"
#include "wx/zipstrm.h"
#include "wx/sstream.h"
#include "wx/txtstrm.h"
#include "wx/textfile.h"
#include "wx/html/htmlpars.h"

#include "ax/ax_utils.h"
#include "ax/ax_activator.h"
#include "ax/ax_pagerctrl.h"
#include "ax/ax_view_summary.h"
#include "ax/ax_summaryviewpanel.h"

#include "tts/tts_htmlparser.h"

#include "ax_resource/ax_resource_library.h"
#include "ax_resource/ax_module_book.h"
#include "ax_resource/ax_activator_resource.h"
#include "ax_resource/ax_menu_resource.h"

#include "epubutils/epub_utils.h"

/*
 * AxBookResource class declaration
 * A resource can represent a tool, document, file, web site, etc.
 */

IMPLEMENT_DYNAMIC_CLASS(AxBookResource, AxResource)

AxBookResource::~AxBookResource()
{
}

void AxBookResource::Init()
{
}

// Copy the basic resource information.
void AxBookResource::Copy(const AxBookResource& resource)
{
    AxResource::Copy(resource);
}

// Copy from epub information.
void AxBookResource::Copy(const ebBasicInfo& info)
{
    SetProperty(axPROPERTY_TITLE, info.GetTitle());
    SetProperty(axPROPERTY_IDENTIFIER, info.GetId());
    SetProperty(axPROPERTY_LANGUAGE, info.GetLanguage());
    SetProperty(axPROPERTY_AUTHOR, info.GetAuthor());
    SetProperty(axPROPERTY_CONTRIBUTORS, info.GetContributors());
    SetProperty(axPROPERTY_PUBLISHER, info.GetPublisher());
    SetProperty(axPROPERTY_SUBJECT, info.GetSubject());
    SetProperty(axPROPERTY_DESCRIPTION, info.GetDescription());
    SetProperty(axPROPERTY_CREATION_DATE, info.GetDate());
    SetProperty(axPROPERTY_RIGHTS, info.GetRights());
    SetProperty(axPROPERTY_COVERAGE, info.GetCoverage());
    //SetProperty(axPROPERTY_TYPE, info.GetType());
    SetProperty(axPROPERTY_FORMAT, info.GetFormat());
    SetProperty(axPROPERTY_SOURCE, info.GetSource());
    //SetProperty(axPROPERTY_RELATION, info.GetRelation());
    SetProperty(axPROPERTY_SOURCE_URL, info.GetUrl());
}

/// Is this an epub?
bool AxBookResource::IsEpub() const
{
    wxString ext = AxFileUtilities::GetExtension(GetFilename()).Lower();
    return ext == wxT("epub");
}


/*
 * AxBookModule class declaration
 * A module for dealing with audio files
 */

IMPLEMENT_DYNAMIC_CLASS(AxBookModule, AxModule)

// Constructor.
AxBookModule::AxBookModule()
{
}

// Called on IDE startup, before the main window has been created.
bool AxBookModule::OnInit()
{
    return true;
}

// Called on application startup, after the main window has been created.
bool AxBookModule::OnInitUI()
{
    return true;
}

// Called just before program termination, but only if OnInit()
// succeeded.
void AxBookModule::OnExit()
{
}

// Read config settings
bool AxBookModule::LoadConfig(wxConfigBase& config)
{
    wxUnusedVar(config);
    return true;
}

// Write config settings
bool AxBookModule::SaveConfig(wxConfigBase& config)
{
    wxUnusedVar(config);
    return true;
}

// Is this file supported?
bool AxBookModule::IsFileSupported(const wxString& filename)
{
    wxString ext(AxFileUtilities::GetExtension(filename).Lower());
    if (GetLibrary()->GetSettings().GetSupportedFiles().IsEnabled(ext) &&
        (ext == wxT("epub") || ext == wxT("mobi") || ext == wxT("prc") ||
         ext == wxT("daisy") || ext == wxT("html") || ext == wxT("htm") ||
         ext == wxT("txt") || ext == wxT("lit")))
        return true;
    else
        return false;
}

// Load the file into a new AxResource
AxResource* AxBookModule::LoadResource(const wxString& filename)
{
    wxString ext = AxFileUtilities::GetExtension(filename).Lower();
    AxBookResource* resource = new AxBookResource;
    resource->SetFilename(filename);
    bool loadedOk = false;
    if (ext == wxT("epub"))
    {
        wxSize thumbnailImageSize(200, 200);
        bool foundCoverImage = false;
        loadedOk = LoadEpubBookInfo(filename, * resource, true /* make thumbnail */, thumbnailImageSize, foundCoverImage);
    }

    // TODO: other formats: HTML, etc.

    if (!loadedOk)
    {
        wxString title(wxFileNameFromPath(filename));
        resource->SetProperty(axPROPERTY_TITLE, title);
        resource->SetProperty(axPROPERTY_FORMAT, ext);
    }

    return resource;
}

// Aggregate any files that belong together and add to library. Return false if you don't
// aggregate any files.
bool AxBookModule::AggregateFiles(AxResourceLibrary* library, wxArrayString& files, bool checkIfFileLoaded)
{
    // TODO
    return false;
}

// Create an activator for the resource, for creating views and further menu items.
AxActivator* AxBookModule::CreateActivator(AxResourceLibrary* library, AxResource* resource)
{
    AxBookResource* bookResource = wxDynamicCast(resource, AxBookResource);
    if (bookResource)
    {
        AxBookResourceActivator* activator = new AxBookResourceActivator(library, bookResource);
        return activator;
    }
    else
        return NULL;
}

// Load information from epub file
bool AxBookModule::LoadEpubBookInfo(const wxString& filename, AxBookResource& resource, bool makeThumbnail, const wxSize& thumbnailImageSize, bool& foundCoverImage)
{
    ebEpubParser parser;
    ebBasicInfo basicInfo;
    wxString coverImageZipFilename;
    if (parser.ReadBasicInfo(filename, basicInfo, coverImageZipFilename))
    {
        resource = basicInfo;
        
        if (!coverImageZipFilename.IsEmpty())
        {
            wxImage image;
            wxImageBlock imageBlock;
            if (parser.CreateImageBlockFromZipImage(filename, coverImageZipFilename, imageBlock, image) && image.IsOk())
            {
                int maxWidth = thumbnailImageSize.x;
                int maxHeight = thumbnailImageSize.y;
                
                double scaleX = ((double) maxWidth)/((double) image.GetWidth());
                double scaleY = ((double) maxHeight)/((double) image.GetHeight());
                
                if (scaleX < 1.0 || scaleY < 1.0)
                {
                    double scale = wxMin(scaleX, scaleY);
                    int newWidth = (int) (scale * image.GetWidth());
                    int newHeight = (int) (scale * image.GetHeight());
                    image.Rescale(newWidth, newHeight, wxIMAGE_QUALITY_HIGH);
                }

                // TODO
#if 0
                info.m_thumbnailImageBlock.MakeImageBlock(image);
#endif
                foundCoverImage = true;
            }
        }

        return true;
    }
    else
    {
        return false;
    }
}

/*
 * AxBookResourceActivator implementation.
 * Activates a book resource.
 */

IMPLEMENT_DYNAMIC_CLASS( AxBookResourceActivator, AxResourceActivator )

AxBookResourceActivator::AxBookResourceActivator(AxResourceLibrary* library, AxResource* resource)
{
    m_library = library;
    m_resource = resource;
}

// Operations

void AxBookResourceActivator::Init()
{
}

void AxBookResourceActivator::Copy(const AxBookResourceActivator& activator)
{
    AxResourceActivator::Copy(activator);
}

AxMenu* AxBookResourceActivator::Activate(AxMenuController* controller, AxMenuItem* item)
{
    AxBookResource* bookResource = GetBookResource();

    // Activate the summary view.
    AxBookView* view = wxDynamicCast(controller->GetViewManager()->FindOrCreateView(CLASSINFO(AxBookView)), AxBookView);
    if (view)
    {
        controller->GetViewManager()->ActivateView(view, m_resource);
        view->ShowBookDetails(bookResource, false /* don't say */);
    }

    AxMenu* menu = new AxMenu;
    menu->SetLabel(item->GetLabel());
    menu->SetDescription(item->GetDescription());

    if (bookResource && bookResource->IsEpub())
    {
        wxString filename = bookResource->GetFilename();
        ebBasicInfo info;
        ebFileLocations locations;
        wxString coverImageFilename, coverPageFilename, opfLocation;

        ebEpubParser epubParser;
        if (epubParser.ReadAdvancedInfo(filename, info, locations, opfLocation, coverImageFilename, coverPageFilename))
        {
            AxMenuItem* menuItem = new AxMenuItem(_("Show book details"), _("Shows detailed information about this book."),
                                        wxEmptyString,
                                        new AxBookResourceCommandActivator(AxBookResourceCommandShowDetails, m_library, m_resource));
            menu->AddItem(menuItem);
    
            size_t i;
            wxString contentPath = wxPathOnly(opfLocation);

            // An array of menus to support creation of a hierarchical structure from file level specifications
            AxMenu* menus[100];
            for (i = 0; i < 100; i++)
                menus[i] = NULL;

            menus[0] = menu;

            int currentLevel = 0;
            int lastLevel = 0;

            AxMenu* currentMenu = menu;
            AxMenuItem* currentMenuItem = NULL;

            for (i = 0; i < locations.m_htmlFiles.GetCount(); i++)
            {
                const ebHtmlFile& file = locations.m_htmlFiles[i];
                if (file.IsHtmlFile())
                {
                    int level = file.m_level - 1;
                    if (level > lastLevel)
                    {
                        if (currentMenuItem)
                        {
                            AxMenu* newMenu = new AxMenu;
                            newMenu->SetLabel(currentMenuItem->GetLabel());
                            newMenu->SetDescription(currentMenuItem->GetDescription());
                            currentMenuItem->SetSubMenu(newMenu);
                            menus[level] = newMenu;

                            currentMenu = newMenu;
                        }
                    }

                    level = wxMax(0, level);

                    currentMenu = menus[level];

                    if (!currentMenu)
                        currentMenu = menu;
                    

                    wxString pageFilename = contentPath + wxT("/") + file.GetRelativeFile(locations);
                    wxString title = file.m_tocTitle;

                    // Get the title
                    {
                        wxFFileInputStream stream(filename);
                        if (!stream.IsOk())
                            return false;
                        
                        wxZipInputStream zipStream(stream);
                        if (!zipStream.IsOk())
                            return false;
                        
                        wxZipEntry* entry = AxArchiveUtilities::FindEntry(zipStream, pageFilename);
                        if (entry)
                        {
                            wxString tempHtmlFile = wxFileName::CreateTempFileName(wxT("epubpage"));
                            AxArchiveUtilities::CopyEntryToFile(zipStream, tempHtmlFile, entry);
                            wxString htmlTitle = ebFindTitle(tempHtmlFile);
                            wxRemoveFile(tempHtmlFile);
                            if (!htmlTitle.IsEmpty())
                                title = htmlTitle;

                            delete entry;
                        }
                    }
                    
                    AxMenuItem* menuItem = new AxMenuItem(title, _("Shows this section."),
                        wxEmptyString,
                        new AxBookResourceCommandActivator(AxBookResourceCommandShowSection, m_library, m_resource, pageFilename));

                    currentMenu->AddItem(menuItem);
                    currentMenuItem = menuItem;
                    lastLevel = level;
                }
            }
        }
    }

    // Add an 'Up' item
    menu->AddItem(new AxGoUpMenuItem);

    return menu;
}

// Called when a menu is reactivated, for example by going up. This
// allows a view to be shown again, without generating all the menus.
bool AxBookResourceActivator::Reactivate(AxMenuController* controller, AxMenuItem* WXUNUSED(item))
{
    // Activate the audio view, with tracks and player.
    AxBookView* view = wxDynamicCast(controller->GetViewManager()->FindOrCreateView(CLASSINFO(AxBookView)), AxBookView);
    if (view && !view->IsActive())
    {
        controller->GetViewManager()->ActivateView(view, m_resource);
    }
    return true;
}

AxBookResource* AxBookResourceActivator::GetBookResource() const
{
    return wxDynamicCast(GetResource(), AxBookResource);
}


/*
 * AxBookResourceCommandActivator implementation.
 * Activates individual menu commands for individual audio resource,
 * for example Play or Show Details.
 */

IMPLEMENT_DYNAMIC_CLASS( AxBookResourceCommandActivator, AxResourceActivator )

AxBookResourceCommandActivator::AxBookResourceCommandActivator(AxBookResourceCommand cmd, AxResourceLibrary* library, AxResource* resource,
                                                               const wxString& sectionLocation):
    AxResourceActivator(library, resource)
{
    m_command = cmd;
    m_sectionLocation = sectionLocation;
}

// Operations

void AxBookResourceCommandActivator::Init()
{
    m_command = AxBookResourceCommandUnknown;
}

void AxBookResourceCommandActivator::Copy(const AxBookResourceCommandActivator& activator)
{
    AxResourceActivator::Copy(activator);

    m_command = activator.m_command;
    m_sectionLocation = activator.m_sectionLocation;
}

AxMenu* AxBookResourceCommandActivator::Activate(AxMenuController* controller, AxMenuItem* WXUNUSED(item))
{
    AxBookView* view = wxDynamicCast(controller->GetViewManager()->FindOrCreateView(CLASSINFO(AxBookView)), AxBookView);

    if (m_command == AxBookResourceCommandPlay)
    {
        //view->PlayOrPause(m_resource);
    }
    else if (m_command == AxBookResourceCommandStop)
    {
        //view->Stop();
    }
    else if (m_command == AxBookResourceCommandShowDetails)
    {
        view->ShowBookDetails(GetBookResource());
    }
    else if (m_command == AxBookResourceCommandShowSection)
    {
        view->ShowBookSection(GetBookResource(), m_sectionLocation);
    }

    return NULL;
}

AxBookResource* AxBookResourceCommandActivator::GetBookResource() const
{
    return wxDynamicCast(GetResource(), AxBookResource);
}

/*
 * AxBookView implementation
 * Handles summary UI
 */

IMPLEMENT_DYNAMIC_CLASS( AxBookView, AxView )

void AxBookView::Init()
{
    SetChannel(AxSpeechContentChannel);
}

AxBookResource* AxBookView::GetResource() const
{
    AxBookResource* resource = wxDynamicCast(GetDocument(), AxBookResource);
    return resource;
}

bool AxBookView::Activate(AxViewManager* viewManager, AxDocument* doc)
{
    // In case the window hasn't been created yet
    Initialize(viewManager);

    AssociateDocument(doc);

    AxBookResource* resource = GetResource();

    AxSummaryViewPanel* panel = GetViewPanel();
    if (panel)
    {
        ShowWindow(viewManager);
        if (resource)
            ShowBookDetails(resource, false /* don't speak */);
    }

    return true;
}

bool AxBookView::Deactivate(AxViewManager* WXUNUSED(viewManager))
{
    // Clear the UI
    AxSummaryViewPanel* panel = GetViewPanel();
    if (panel)
        panel->m_textCtrl->SetValue(wxEmptyString);
    return true;
}

void AxBookView::ShowViewIfInactive()
{
    if (!IsActive())
        GetViewManager()->ActivateView(this, GetDocument());
}

bool AxBookView::Initialize(AxViewManager* viewManager)
{
    if (!GetWindow())
    {
        AxSummaryViewPanel* panel = wxDynamicCast(viewManager->GetViewPagerCtrl()->FindPageWindow(wxT("Summary")), AxSummaryViewPanel);
        if (!panel)
        {
            panel = new AxSummaryViewPanel(viewManager->GetViewPagerCtrl(), wxID_ANY);
            viewManager->GetViewPagerCtrl()->AddPage(panel, wxT("Summary"));
        }
        SetWindow(panel);
    }
    return true;
}

bool AxBookView::Uninitialize(AxViewManager* WXUNUSED(viewManager))
{
    return true;
}

AxSummaryViewPanel* AxBookView::GetViewPanel() const
{
    return wxDynamicCast(GetWindow(), AxSummaryViewPanel);
}

void AxBookView::SetDescription(const wxString& text)
{
    AxSummaryViewPanel* panel = GetViewPanel();
    if (panel)
    {
        panel->m_textCtrl->SetValue(text);
    }
}

bool AxBookView::ShowBookDetails(AxBookResource* resource, bool say)
{
    wxString text;
    size_t i;
    for (i = 0; i < resource->GetProperties().GetCount(); i++)
    {
        const AxProperty& prop = resource->GetProperty(i);
        if (!prop.GetValue().IsEmpty() && prop.GetName() != wxT("Filename"))
        {
            text << prop.GetName() << _(" is ") << prop.GetValue() << wxT(".\n");
        }
    }
    text << _("Filename is ") << resource->GetFilename();

    SetDescription(text);
    if (say)
    {
        StopSpeech(AxSpeechAllChannels);
        Say(text, AxSpeechFlagsPurgeAll);
    }

    return true;
}

bool AxBookView::ShowBookSection(AxBookResource* resource, const wxString& sectionLocation, bool say)
{
    wxString filename = resource->GetFilename();
    if (!filename.IsEmpty() && wxFileExists(filename))
    {
        wxFFileInputStream stream(filename);
        if (!stream.IsOk())
            return false;

        wxZipInputStream zipStream(stream);
        if (!zipStream.IsOk())
            return false;
        
        wxZipEntry* entry = AxArchiveUtilities::FindEntry(zipStream, sectionLocation);
        if (entry)
        {
            wxString tempHtmlFile = wxFileName::CreateTempFileName(wxT("epubpage"));
            wxString tempTextFile = wxFileName::CreateTempFileName(wxT("epubpage_text"));

            AxArchiveUtilities::CopyEntryToFile(zipStream, tempHtmlFile, entry);

            {
                wxFFileOutputStream outputStream(tempTextFile);
                
                if (ConvertHTMLToText(tempHtmlFile, outputStream))
                {
                }
            }

            wxString text;

            // Now do some post-processing to remove unnecessary space
            {
                wxTextFile textFile;
                if (textFile.Open(tempTextFile))
                {
                    int newLineCount = 0;
                    bool startedContent = false;
                    size_t i;
                    for (i = 0; i < textFile.GetLineCount(); i++)
                    {
                        wxString line = textFile[i];
                        if (line != wxEmptyString)
                        {
                            line.Trim(true);
                            line.Trim(false);
                            textFile[i] = line;
                        }

                        if (line == wxEmptyString)
                        {
                            newLineCount ++;
                            if (newLineCount > 2)
                            {
                                // Don't add
                            }
                            else if (startedContent)
                            {
                                text << wxT("\n");
                            }
                        }
                        else
                        {
                            text << line << wxT("\n");
                            newLineCount = 0;
                            startedContent = true;
                        }
                    }
                }
            }

            wxRemoveFile(tempHtmlFile);
            wxRemoveFile(tempTextFile);

            SetDescription(text);
            if (say)
            {
                StopSpeech(AxSpeechAllChannels);
                Say(text, AxSpeechFlagsPurgeAll);
            }

            delete entry;

            return true;
        }
    }

    return false;
}

// Remove HTML tags from file, outputting text to stream
bool AxBookView::ConvertHTMLToText(const wxString& filename, wxOutputStream& outputStream, int WXUNUSED(options))
{
    wxTextOutputStream stream(outputStream);

    // Mapping for entities that aren't spoken properly by SAPI
    wxArrayString entities;
    wxArrayString equivalents;

    // TODO: expand this, and separate it out
    entities.Add(wxT("copy"));
    equivalents.Add(wxT("copyright"));

    entities.Add(wxT("nbsp"));
    equivalents.Add(wxT(" "));

    wxTTSSimpleHtmlParser parser;
    wxArrayString toReplace, replaceWith;

    wxHtmlEntitiesParser entitiesParser;
    
    if (parser.ParseFile(filename))
    {
        wxTTSSimpleHtmlTag* tag = parser.GetTopLevelTag()->GetChildren();

        int currentListLevel = -1;
        wxArrayInt      listLevels;
        wxArrayString   listTypes;
        wxString lastText;
        bool            suppressText = false;

        while (tag)
        {
            if ((tag->NameIs(wxT("ol")) || tag->NameIs(wxT("ul"))) && tag->GetType() == wxTTSSimpleHtmlTag_Open)
            {
                stream << wxT("\n");
                currentListLevel ++;
                if ((int) listLevels.GetCount() <= currentListLevel)
                {
                    listLevels.Add(0);
                    listTypes.Add(tag->GetName().Lower());
                }
                else
                {
                    listLevels[currentListLevel] = 0;
                    listTypes[currentListLevel] = tag->GetName().Lower();
                }

            }
            else if ((tag->NameIs(wxT("ol")) || tag->NameIs(wxT("ul"))) && tag->GetType() == wxTTSSimpleHtmlTag_Close)
            {
                currentListLevel --;
                if (currentListLevel < -1)
                    currentListLevel = -1;
            }
            else if (tag->NameIs(wxT("li")) && tag->GetType() == wxTTSSimpleHtmlTag_Open)
            {
                if (currentListLevel >= 0)
                {
                    listLevels[currentListLevel] ++;

                    if (currentListLevel >= 0 && listTypes[currentListLevel] == wxT("ol"))
                    {
                        stream << wxString::Format(wxT("%d. "), listLevels[currentListLevel]);
                    }
                }
            }
            else if (tag->NameIs(wxT("p")) && tag->GetType() == wxTTSSimpleHtmlTag_Close)
            {
                stream << wxT("\n\n");
            }
            else if (tag->NameIs(wxT("br")))
            {
                stream << wxT("\n");
            }
            else if (tag->NameIs(wxT("title")))
            {
                if (tag->GetType() == wxTTSSimpleHtmlTag_Open)
                    suppressText = true;
                else
                    suppressText = false;
            }
            else if (tag->NameIs(wxT("h1")) || tag->NameIs(wxT("h2")) || tag->NameIs(wxT("h3")) || tag->NameIs(wxT("h4")) || tag->NameIs(wxT("h5")))
            {
                if (tag->GetType() == wxTTSSimpleHtmlTag_Open)
                {
                    stream << wxT("\n");
                    if (0) // options & wxTTS_TRANSFORM_SPEAK_HEADINGS)
                    {
                        wxString headerNumberStr(tag->GetName().Mid(1));
                        int headingNumber = wxAtoi(headerNumberStr);
                        stream << wxT("Heading ") << headingNumber << wxT(": ");
                    }
                }
                else
                {
                    // Add punctuation to the end of a heading for correct intonation
                    if (lastText.Length() > 0)
                    {
                        wxChar ch = lastText.Last();
                        if (ch != wxT('.') && ch != wxT(',') && ch != wxT('!') && ch != wxT('?'))
                            stream << wxT(".");
                    }

                    stream << wxT("\n\n");
                }
            }
            else if (tag->GetType() == wxTTSSimpleHtmlTag_Text)
            {
                if (!suppressText)
                {
                    lastText = tag->GetText();
                    lastText.Replace(wxT("\n"), wxT(" "));
                    stream << lastText;
                }
            }
            else if (tag->GetType() == wxTTSSimpleHtmlTag_Entity)
            {
                if (!suppressText)
                {
                    wxChar ch = entitiesParser.GetEntityChar(tag->GetName());
                    if (ch != 0)
                    {
                        if (ch == 160) // nbsp
                            ch = wxT(' ');
                        
                        lastText = wxEmptyString;
                        lastText += ch;

                        // If we find an equivalent for the entity, use that
                        // instead, in either XML or text mode.

                        int pos = entities.Index(tag->GetName());
                        if (pos != wxNOT_FOUND)
                            lastText = equivalents[pos];
                        
                        stream << lastText;
                    }
                }
            }

            tag = tag->GetNext();
        }
    }
    else
        return false;

    return true;
}


// Remove redundant symbols from ISBN number
wxString AxMakeISBNGood(const wxString& isbn)
{
    wxString isbn2(isbn);
    isbn2.Replace(wxT("ISBN"), wxEmptyString);
    isbn2.Replace(wxT(" "), wxEmptyString);
    isbn2.Replace(wxT("-"), wxEmptyString);
    isbn2.Trim(true);
    isbn2.Trim(false);
    return isbn2;
}

// Is this a valid ISBN?
bool AxIsISBN(const wxString& isbn)
{
    size_t i;
    for (i = 0; i < isbn.Length(); i++)
    {
        wxChar ch = isbn[i];
        if (!wxIsdigit(ch) && ch != wxT('x') && ch != wxT('x'))
            return false;
    }
    return true;
}
